package club.jdiy.core.storage;

import club.jdiy.core.properties.JDiyStoreProperties;
import club.jdiy.core.ex.JDiyException;
import club.jdiy.utils.FileUtils;
import club.jdiy.utils.IdWorker;
import club.jdiy.utils.JsonUtils;
import club.jdiy.utils.Md5Utils;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * JDiy 七牛云对象存储实现类.
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20200725
 */
@SuppressWarnings("ALL")
public class KodoStore extends StoreSaver implements Store {
    private StoreMetaData storeMetaData;


    public KodoStore(JDiyStoreProperties.Kodo kodo, StoreMetaData storeMetaData, String storeBucket, String id) {
        this.kodo = kodo;
        this.storeMetaData = storeMetaData;
        this.storeBucket = storeBucket;
        this.dataId = id;

        String s = kodo.getDomain();
        if (!kodo.getDomain().endsWith("/")) kodo.setDomain(kodo.getDomain() + "/");

        try {
            this.region = (Region) Region.class.getMethod(kodo.getRegion().name()).invoke(Region.class);
        } catch (Exception ex) {
            throw new JDiyException("获取七牛存储区域信息(Region)失败．");
        }
    }


    public StoreMetaData getStoreData() {
        return storeMetaData;
    }

    @Override
    String getDataId() {
        return dataId;
    }

    /**
     * view层调用核心方法．使用方式：
     * 1. DBEntity继承 {@link AbstractStorble} 或实现 {@link Storable}接口.
     * 2. 然后在Controller中调用 DBEntity对象的.init方法
     * 3. view中取得单一附件（例如获取listPic附件的下载地址）:   ${vo.store.listPic}  或者：  ${vo.store['listPic']}
     * view中取得多个附件（例如获取listPic附件的下载地址）:  ${vo.store.listPic.items[0]}
     *
     * @param field 附件字段名．对应于 ${vo.store.xxx}中的xxx
     * @return Entry
     */
    //不要更改和移除(view层调用核心方法),
    // 可以直接使用 store[field]来获取对应的Attch
    //获取单个附件字段的Store
    public Store.Entry get(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit == null) return new Store.Entry(new Store.EntryItem[0]);

        Store.EntryItem[] entries = new Store.EntryItem[aiit.size()];
        int i = 0;
        for (StoreMetaData.StoreItem ai : aiit) {
            entries[i++] = new Store.EntryItem(ai, kodo.getDomain() + ai.getKey());
        }
        return new Store.Entry(entries);
    }

    public String getUrl(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        return aiit == null || aiit.size() < 1 ? null : kodo.getDomain() + aiit.get(0).getKey();
    }

    public String[] getUrls(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit == null || aiit.size() < 1) return null;
        int i = 0;
        String[] urls = new String[aiit.size()];
        for (StoreMetaData.StoreItem ai : aiit) urls[i++] = kodo.getDomain() + ai.getKey();
        return urls;
    }

    public void set(String field, File file) throws IOException {
        this.set(field, file, null);
    }

    public void set(String field, File file, String fileName) throws IOException {
        __saveFile(field, file, fileName, false);
    }

    public void add(String field, File file) throws IOException {
        this.add(field, file, null);
    }

    public void add(String field, File file, String fileName) throws IOException {
        __saveFile(field, file, fileName, true);
    }

    public void set(String field, MultipartFile mFile) throws IOException {
        this.set(field, mFile, null);
    }

    public void set(String field, MultipartFile mFile, String fileName) throws IOException {
        this.set(field, mFile, fileName,  null);
    }
    /**
     * @param field    文件字段名
     * @param mFile    上传附件
     * @param allowExt 　逗号分隔的文件扩展名列表，为null不限制
     * @param c 　用户可对保存的文件进行一定的处理（如图片加水印，压缩尺寸等）
     * @return 当前持久化保存的附件
     * @throws IOException IO
     */
    public void set(String field, MultipartFile mFile, String fileName,Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, false, c);
    }

    /**
     * @param field    文件字段名
     * @param mFile    上传附件
     * @param allowExt 　逗号分隔的文件扩展名列表，为null不限制
     * @return 当前持久化保存的附件
     * @throws IOException IO
     */
    public void add(String field, MultipartFile mFile) throws IOException {
        __saveFile(field, mFile, null, true, null);
    }

    public void add(String field, MultipartFile mFile, String fileName) throws IOException {
        this.add(field, mFile, fileName, null);
    }

    public void add(String field, MultipartFile mFile, String fileName, Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, true, c);
    }

    public void del(String field) {
        if (field == null || "".equals(field)) return;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) remove(ai.getKey());
            storeMetaData.getItems().remove(field);
        }
    }

    public void del(String field, String key) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    remove(key);
                    aiit.remove(ai);
                    break;
                }
            }
            if (aiit.size() < 1) storeMetaData.getItems().remove(field);
        }
    }

    public void delAll() {
        for (List<StoreMetaData.StoreItem> aiit : storeMetaData.getItems().values()) {
            if (aiit != null) {
                for (StoreMetaData.StoreItem ai : aiit) remove(ai.getKey());
            }
        }
        storeMetaData.setItems(new HashMap<>());
    }

    @Override
    public String put(MultipartFile file, String fileName) throws IOException {
        return put(file, fileName, null);
    }

    @Override
    public String put(MultipartFile file, String fileName, Consumer<File> c) throws IOException {
        String field = IdWorker.getId() + "";
        __saveFile(field, file, fileName, true, c);
        return getUrl(field);
    }

    @Override
    public String put(File file, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        set(field, file, fileName);
        return getUrl(field);
    }

    @Override
    public String put(byte[] bytes, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        return kodo.getDomain() + save(field, fileName, bytes);
    }

    private void __saveFile(String field, File file, String fileName, boolean isAdd) throws IOException {
        if (file == null || field == null || "".equals(field) || !file.exists() || !file.isFile()) return;
        boolean autoRename = fileName == null || "".equals(fileName);
        if (autoRename) fileName = file.getName();
        String ext = file.getName().substring(file.getName().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        if (autoRename && !Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
            }
        }
        if (!isAdd) del(field);
        String key = save(field, fileName, file);
        boolean has = false;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    has = true;
                    break;
                }
            }
        } else {
            storeMetaData.getItems().put(field, new ArrayList<>());
        }
        if (!has) storeMetaData.getItems().get(field).add(new StoreMetaData.StoreItem(key, file.length(), ext));
    }

    private void __saveFile(String field, MultipartFile mFile, String fileName, boolean isAdd, Consumer<File> c) throws IOException {
        if (mFile == null || mFile.isEmpty()) return;
        if (fileName == null || "".equals(fileName)) fileName = mFile.getOriginalFilename();
        String ext = mFile.getOriginalFilename().substring(mFile.getOriginalFilename().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        //zlt20230610:强制更名不合理，已取消此操作。
//        if (!Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
//            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
//                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
//            }
//        }

        if (!isAdd) del(field);
        String key;
        if (c == null) {
            key = save(field, fileName, mFile);
        } else {
            File dir = new File("/tmp/" + UUID.randomUUID());
            dir.mkdirs();
            File file = new File(dir.getAbsolutePath() + "/" + fileName);
            try (InputStream inputStream = mFile.getInputStream(); OutputStream outputStream = new FileOutputStream(file)) {
                byte[] bs = new byte[1024];
                int res;
                while ((res = inputStream.read(bs)) != -1) {
                    outputStream.write(bs, 0, res);
                }
                outputStream.flush();
            }
            if (c != null) c.accept(file);
            key = save(field, fileName, file);
            file.delete();
            FileUtils.delEmpty(file.getParentFile());
        }
        boolean has = false;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    has = true;
                    break;
                }
            }
        } else {
            storeMetaData.getItems().put(field, new ArrayList<>());
        }
        if (!has) storeMetaData.getItems().get(field).add(new StoreMetaData.StoreItem(key, mFile.getSize(), ext));
    }


    private void remove(String key) {
        if (Boolean.FALSE.equals(kodo.getDelete())) return;
        Configuration cfg = new Configuration(region);
        //...其他参数参考类注释
        Auth auth = Auth.create(kodo.getAccessKey(), kodo.getSecretKey());
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.delete(kodo.getBucket(), key);
        } catch (QiniuException ex) {
            //如果遇到异常，说明删除失败
            //System.err.println(ex.code());
            //System.err.println(ex.response.toString());
        }
    }

    private String save(String field, String fileName, File file) throws IOException {
        Configuration cfg = new Configuration(region);//华南空间
        UploadManager uploadManager = new UploadManager(cfg);
        String key = storeBucket + "/" + dataId + "/" + field + "/" + fileName;
        Auth auth = Auth.create(kodo.getAccessKey(), kodo.getSecretKey());
        String upToken = auth.uploadToken(kodo.getBucket());
        Response response = uploadManager.put(file, key, upToken);
        DefaultPutRet putRet = JsonUtils.parse(response.bodyString(), DefaultPutRet.class);
        return putRet.key;
    }

    private String save(String field, String fileName, byte[] bytes) throws IOException {
        Configuration cfg = new Configuration(region);//华南空间
        UploadManager uploadManager = new UploadManager(cfg);
        String key = storeBucket + "/" + dataId + "/" + field + "/" + fileName;
        Auth auth = Auth.create(kodo.getAccessKey(), kodo.getSecretKey());
        String upToken = auth.uploadToken(kodo.getBucket());
        Response response = uploadManager.put(bytes, key, upToken);
        DefaultPutRet putRet = JsonUtils.parse(response.bodyString(), DefaultPutRet.class);
        return putRet.key;
    }

    private String save(String field, String fileName, MultipartFile mFile) throws IOException {
        return save(field, fileName, mFile.getBytes());
    }

    private JDiyStoreProperties.Kodo kodo;
    private final String storeBucket;
    private final String dataId;
    private final Region region;
}

